﻿//Copyright (c) 2009, Andreas Grünwald
//All rights reserved.

using System;
using System.Linq;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using yaTDL.Core.Toodledo;
using yaTDL.Core.IO;
using yaTDL.Extensions;

#if !SILVERLIGHT
using yaTDL.Core.Properties;
using Slf;
using Slf.Resolvers;
using Slf.Factories;
using System.IO;
using System.Timers;
using System.ComponentModel;
#endif

namespace yaTDL.Core
{
    /// <summary>
    /// A static class which acts as a central point for accessing Tasks and Projects.
    /// </summary>        
    public static class TaskStore
    {       

        /// <summary>
        /// The Dicitonary that is used to store all Projects.
        /// </summary>
        private static Dictionary<String, Project> projects = new Dictionary<string, Project>();

        private static Dictionary<String, Task> tasks = new Dictionary<string, Task>();

        private static Collection<Change> changes = new Collection<Change>();
 
        /// <summary>
        /// Indicates whether fileaccess.SaveProjects() will be executed.
        /// It has to be set to false while data is being read from the file during startup, because the file is locked.
        /// </summary>
        private static bool enableSaving = false;

        /// <summary>
        /// Stores when data has been written to disk the last time. 
        /// </summary>
        private static DateTime lastWrite;

#if SILVERLIGHT
        /// <summary>
        /// Used to read and write taskStore's data from/to a file.
        /// </summary>
        private static Fileaccess fileaccess = new XMLFileaccess();
#else
        /// <summary>
        /// Used to read and write taskStore's data from/to a file.
        /// </summary>
        private static FileHandler fileaccess = new XMLFileHandler(true);

        private static ILogger logger = LoggerService.GetLogger();
        
        private static BackupService backupService = new BackupService(fileaccess.DefaultFileName);

        public static ToodledoSyncServiceV2 SyncService { get; private set; }
#endif



        #region Events

        /// <summary>
        /// A event that is raised when a new project is added. 
        /// </summary>
        public static event EventHandler<ProjectEventArgs> ProjectAdded;       
        
        /// <summary>
        /// A event that is raised when a Project has been deleted.
        /// </summary>
        public static event EventHandler<ProjectEventArgs> ProjectDeleted;


        /// <summary>
        /// A event that will be raised when a Task is added. 
        /// It will be raised when a TaskAdded event is raised in one of TaskStore's Projects.       
        /// </summary>
        public static event EventHandler<TaskEventArgs> TaskAdded;        

        /// <summary>
        /// A event that will be raised when a Task is deleted.
        /// It will be raised when a TaskDeleted event is raised in one of TaskStore's Projects.       
        /// </summary>
        public static event EventHandler<TaskEventArgs> TaskDeleted;

        public static event EventHandler<PropertyChangedEventArgs> TaskPropertyChanged;

        #endregion


        /// <summary>
        /// Initializes TaskStore.
        /// </summary>
        static TaskStore()
        {            

            if (!File.Exists(fileaccess.DefaultFileName))
            {
                logger.Info("Default xml file does not exist.");
                TextFileHandler txtReader = new TextFileHandler();

                if (File.Exists(txtReader.DefaultFileName))
                {
                    logger.Info("Found txt file. Trying to import");
                    txtReader.OpenFile();
                    enableSaving = true;
                    save();
                }
            }
            else
            {
                //loads the tasks from file at startup
                logger.Info("Loading file: {0}", fileaccess.DefaultFileName);
                fileaccess.OpenFile();
            }


            ApplicationEvents.ApplicationClosing += new EventHandler(ApplicationEvents_ApplicationClosing);

#if !SILVERLIGHT
            SyncService = new ToodledoSyncServiceV2();
            SyncService.ActionStarted += new EventHandler(SyncService_ActionStarted);
            SyncService.ActionCompleted += new EventHandler(SyncService_ActionCompleted);          
#endif
            enableSaving = true;
            lastWrite = DateTime.Now;
        }

            


        #region Task-related public Methods

        /// <summary>
        /// Adds a new Task
        /// </summary>
        /// <param name="projectId">Specifies the Project the Task will be added to</param>
        /// <param name="newTask">The task that will be added</param>
        public static void AddTask(Task newTask)
        {
            if (tasks.ContainsKey(newTask.Id))
            {
                if (newTask.LastEdited > tasks[newTask.Id].LastEdited)
                {
                    tasks[newTask.Id].Content = newTask.Content;
                    tasks[newTask.Id].DueDate = newTask.DueDate;
                    tasks[newTask.Id].Recurrence = newTask.Recurrence;

                    tasks[newTask.Id].Completed = newTask.Completed;
                    tasks[newTask.Id].Priority = newTask.Priority;
                    tasks[newTask.Id].LastEdited = newTask.LastEdited;
                }
            }
            else
            {
                tasks.Add(newTask.Id, newTask);
                newTask.IdChanged += new EventHandler<IdChangedEventArgs>(Task_IdChanged);
                newTask.PropertyChanged += new PropertyChangedEventHandler(Task_TaskPropertyChanged);

                Change c = new Change()
                {
                    ChangedItemType = typeof(Task),
                    ChangeTime = DateTime.Now,
                    Kind = ChangeKind.Added,
                    ItemId = newTask.Id,
                    PropertyName = ""
                };
                AddChange(c);

                OnTaskAdded(newTask);
            }

            save();
        }

        public static bool TaskExists(string taskId)
        {
            return tasks.ContainsKey(taskId);
        }

        /// <summary>
        /// Used to get Tasks from TaskStore.
        /// </summary>
        /// <param name="projectId">The identifier of the Project that contains the Tasks.</param>
        /// <param name="isInHistory">Specifies if the completed or uncompleted Tasks are to be returned.</param>
        /// <returns>Returns a List of Tasks that match the indicated criteria.</returns>
        public static Collection<Task> GetTasks(string projectId, bool isInHistory)
        {
            var taskquery = from task in tasks.Values
                            where task.ProjectId == projectId && task.Completed == isInHistory
                            select task;

            return new Collection<Task>(taskquery.ToList<Task>());
        }

        public static Collection<Task> GetTasks()
        {
            return new Collection<Task>(tasks.Values.ToList<Task>());
        }

        public static Collection<Task> GetTasks(string projectId)
        {
            var query = from task in tasks.Values
                        where task.ProjectId == projectId
                        select task;

            return new Collection<Task>(query.ToList<Task>());
        }

        /// <summary>
        /// A method that can be used to get a single Task from a Project.
        /// </summary>
        /// <param name="projectId">The Id of the Project which contains the Task.</param>
        /// <param name="taskId">The Id of the requested Task.</param>
        /// <returns>Returns the Task with the indicated Id from the indicated Project.</returns>
        public static Task GetTask(string taskId)
        {
            if (tasks.ContainsKey(taskId))
            { return tasks[taskId]; }
            else
            { return null; }
        }

        /// <summary>
        /// Removes a task in a Project.
        /// </summary>
        /// <param name="projectId">the identifer of the project from which the task will be removed</param>
        /// <param name="taskId">the identifier of the task that will be removed</param>
        public static void RemoveTask(string taskId)
        {
            if (tasks.ContainsKey(taskId))
            {
                Task t = tasks[taskId];
                tasks[taskId].IdChanged -= Task_IdChanged;
                tasks[taskId].PropertyChanged -= Task_TaskPropertyChanged;
                tasks.Remove(taskId);

                Change c = new Change()
                {
                    Kind = ChangeKind.Deleted,
                    ChangedItemType = typeof(Task),
                    ChangeTime = DateTime.Now,
                    ItemId = taskId,                    
                    PropertyName = ""
                };
                AddChange(c);


                OnTaskDeleted(t);
            }
            
            save();
        }

        /// <summary>
        /// Deletes all completed tasks in a project.
        /// </summary>
        /// <param name="projectId">The identifier of the project which's completed tasks will be deleted.</param>
        public static void DeleteHistory(string projectId)
        {
            enableSaving = false;

            foreach (Task t in tasks.Values)
            {
                if (t.ProjectId == projectId)
                {
                    RemoveTask(t.Id);
                }
            }
            
            enableSaving = true;
            save();
        }

        #endregion

        #region Project-related public Methods

        /// <summary>
        /// Adds a new Project.
        /// If a Project with the same Id already exists, the Pojects will be merged.
        /// </summary>
        /// <param name="newProject">The project that will be added.</param>
        public static void AddProject(Project newProject)
        {
            if(ProjectNameExists(newProject.Name))
            {
                throw new Exception("A Project with that Name already exists");
            }


            if (ProjectExists(newProject.Id))
            {
                if (newProject.LastEdited > GetProject(newProject.Id).LastEdited)
                {
                    Project existingProject = GetProject(newProject.Id);
                    existingProject.Name = newProject.Name;
                    existingProject.Collapsed = newProject.Collapsed;
                    existingProject.Color = newProject.Color;
                    existingProject.Indent = newProject.Indent;
                    existingProject.Order = newProject.Order;
                    existingProject.UserId = newProject.UserId;                    
                }
            }

            else
            {
                projects.Add(newProject.Id, newProject);
                newProject.IdChanged += new EventHandler<IdChangedEventArgs>(Project_IdChanged);
                newProject.PropertyChanged += new PropertyChangedEventHandler(Project_ProjectPropertyChanged);

                Change c = new Change()
                {
                    ChangedItemType = typeof(Project),
                    ChangeTime = DateTime.Now,
                    Kind = ChangeKind.Added,
                    ItemId = newProject.Id,
                    PropertyName = ""
                };
                AddChange(c);
                

                OnProjectAdded(newProject.Id);
            }
        }

        /// <summary>
        /// Checks if a certain Project exists.
        /// </summary>
        /// <param name="projectId">The identifier that is used to indentify the project.</param>
        /// <returns>Returns if the a project with the indicated identifier exists.</returns>
        public static bool ProjectExists(string projectId)
        {
            if (String.IsNullOrEmpty(projectId)) { return false; }
            return projects.ContainsKey(projectId);
        }

        /// <summary>
        /// Checks whether a Project with the indicated name exists.
        /// </summary>
        /// <param name="name">The Name to be looked up.</param>
        /// <returns>Returns true if a mthcing Project exists and false if not.</returns>
        public static bool ProjectNameExists(string name)
        {
            var matchingProjects = from project in projects.Values
                                      where project.Name == name
                                      select project;

            return matchingProjects.Any() ? true : false;
        }

        /// <summary>
        /// Get a Project from TaskStore
        /// </summary>
        /// <param name="projectId">The Id of the Project that is to be returned.</param>
        /// <returns>Returns the requested Project..</returns>
        public static Project GetProject(string projectId)
        {
            if (ProjectExists(projectId))
            {
                return projects[projectId];
            }
            else
            { return null; }
        }

        /// <summary>
        /// A method to get all Projects.
        /// </summary>
        /// <returns>Returns System.Collections.ObjectModel.Collection containing all Projects in TaskStore</returns>
        public static Collection<Project> GetProjects()
        {
            Collection<Project> projectCollection = new Collection<Project>();
            List<Project> projectList = projects.Values.ToList<Project>();

            foreach (Project item in projectList)
            {
                projectCollection.Add(item);
            }

            return projectCollection;           
        }

        /// <summary>
        /// Deletes a Project.
        /// </summary>
        /// <param name="projectId">The identifier of the Project that will be deleted.</param>
        public static void RemoveProject(string projectId)
        {
            if (ProjectExists(projectId))
            {
                projects[projectId].IdChanged -= Project_IdChanged;
                projects.Remove(projectId);

                Change c = new Change()
                {
                    Kind = ChangeKind.Deleted,
                    ChangedItemType = typeof(Project),
                    ChangeTime = DateTime.Now,
                    ItemId = projectId,                    
                    PropertyName = ""
                };
                AddChange(c);

                OnProjectDeleted(projectId);

                foreach (Task t in tasks.Values.Where(t => t.ProjectId == projectId))
                {                    
                    t.ProjectId = "0";
                }

                save();
            }
        }

        #endregion

        #region Change-related public Methods

        /// <summary>
        /// Adds a new Change.
        /// </summary>
        /// <param name="newChange">The Change to be added.</param>
        public static void AddChange(Change newChange)
        {
            //A item can only be added if it has been delted or vice versa,
            //so it needs to be verified that the last change was the corresponding action.
            if (newChange.Kind == ChangeKind.Added || newChange.Kind == ChangeKind.Deleted)
            {
                List<Change> changeQuery = (from change in changes
                                            where change.ItemId == newChange.ItemId && change.ChangedItemType == newChange.ChangedItemType
                                            orderby change.ChangeTime
                                            select change).ToList<Change>();

                if (newChange.Kind == ChangeKind.Added && changeQuery.Any())
                {
                    if (changeQuery.Last().Kind != ChangeKind.Deleted) { return; }
                }

                if (newChange.Kind == ChangeKind.Deleted && changeQuery.Any())
                {
                    if (changeQuery.Last().Kind != ChangeKind.Added) { return; }
                }
            }

            //Check for the last modification of the same item. If the same property was edited only a short time ago, it will only be kept record of one change
            else             
            {
                var lastChangeQuery = (from change in changes
                                     where change.ItemId == newChange.ItemId 
                                     && change.ChangedItemType == newChange.ChangedItemType 
                                     && change.Kind == newChange.Kind
                                     && change.PropertyName == newChange.PropertyName
                                     && (newChange.ChangeTime - change.ChangeTime).TotalMinutes < 5        //if the last change is older than 5 minutes both the last and the new Change will be saved
                                     && change.ChangeTime > ApplicationSettings.Settings.Toodledo_LastSync //only remove change if it is newer than the last sync to avoid sync-errors
                                     orderby change.ChangeTime
                                     select change);

                Change lastChange = lastChangeQuery.Any() ? lastChangeQuery.Last() : null;

                if (lastChange != null)
                {
                    lastChange.PropertyChanged -= Change_PropertyChanged;
                    changes.Remove(lastChange);
                }                               
            }

            newChange.PropertyChanged += new PropertyChangedEventHandler(Change_PropertyChanged);

            changes.Add(newChange);
            save();
        }

        

        /// <summary>
        /// Returns a Collection of all Changes stored in TaskStore.
        /// </summary>
        public static Collection<Change> GetChanges(bool removeDuplicates = false)
        {
            if (removeDuplicates)
            {
                List<string> ids = new List<string>();

                var _changes = from change in changes
                               orderby change.ChangeTime descending
                               select change;

                Collection<Change> result = new Collection<Change>();

                foreach (Change c in _changes)
                {
                    if (!ids.Contains(c.ItemId))
                    { 
                        result.Add(c);
                        ids.Add(c.ItemId);
                    }
                }

                return result;
            }
            else
            {
                return new Collection<Change>(changes);
            }
        }

        /// <summary>
        /// Returns a Colelction of Changes that match the indicated criteria.
        /// </summary>
        /// <param name="itemType">The Type of the changed items.</param>
        /// <param name="itemId">The Id of the changed item.</param>        
        public static Collection<Change> GetChanges(Type itemType, string itemId)
        {
            var query = from change in changes
                        where change.ItemId == itemId && change.ChangedItemType == itemType
                        orderby change.ChangeTime
                        select change;

            return new Collection<Change>(query.ToList<Change>());
        }

        /// <summary>
        /// Gets a the Ids of deleted Projects.
        /// </summary>
        /// <returns>Returns a Collection of Strings with the Ids of deleted Projects.</returns>
        public static Collection<String> GetDeletedProjects()
        {
            return GetDeletedProjects(DateTime.Parse("01.01.0001"));
        }

        /// <summary>
        /// Gets a the Ids of deleted Projects.
        /// </summary>
        /// <param name="after">The DateTime when to start the search for deleted Projects.</param>
        /// <returns>Returns a Collection of Strings with the Ids of delted Projects.</returns>
        public static Collection<String> GetDeletedProjects(DateTime after)
        {
            return getDeletedItems(typeof(Project), after);
        }

        /// <summary>
        /// Gets a the Ids of deleted Tasks.
        /// </summary>
        /// <returns>Returns a Collection of Strings with the Ids of deleted Tasks.</returns>
        public static Collection<String> GetDeletedTasks()
        {
            return GetDeletedTasks(DateTime.Parse("01.01.0001"));
        }

        /// <summary>
        /// Gets a the Ids of deleted Tasks.
        /// </summary>
        /// <param name="after">The DateTime when to start the search for deleted Tasks.</param>
        /// <returns>Returns a Collection of Strings with the Ids of deleted Tasks.</returns>
        public static Collection<String> GetDeletedTasks(DateTime after)
        {
            return getDeletedItems(typeof(Task), after);                    
        }

        private static Collection<String> getDeletedItems(Type type, DateTime after)
        {
            var changeGroups = from change in changes
                               where change.ChangedItemType == type && change.ChangeTime > after
                               group change by change.ItemId
                                   into changeGroup
                                   orderby changeGroup.Key
                                   select changeGroup;

            List<string> deletedItems = new List<string>();

            foreach (IGrouping<String, Change> changeGroup in changeGroups)
            {
                changeGroup.OrderBy<Change, DateTime>(x => x.ChangeTime);

                if (changeGroup.Any())
                {
                    if (changeGroup.Last().Kind == ChangeKind.Deleted) { deletedItems.Add(changeGroup.Last().ItemId); }
                }
            }

            return new Collection<string>(deletedItems);
        }

        #endregion
       

        /// <summary>
        /// Creates a new Backup.
        /// </summary>
        public static void Backup()
        {
            backupService.Run();            
        }


        #region Private Methods

        /// <summary>
        /// Raises the ProjectAdded event.
        /// </summary>
        /// <param name="e"><see cref="ProjectEventArgs"/> for the event</param>
        private static void OnProjectAdded(string projectId)
        {
            if (ProjectAdded != null)
            {
                ProjectAdded(null, new ProjectEventArgs(projectId));
            }
        }

        /// <summary>
        /// Raises the ProjectDeleted event.
        /// </summary>
        /// <param name="e"><see cref="ProjectEventArgs"/> for the event</param>
        private static void OnProjectDeleted(string projectId)
        {
            if (ProjectDeleted != null)
            {
                ProjectDeleted(null, new ProjectEventArgs(projectId));
            }
        }

        /// <summary>
        /// Raises the TaskAdded event.
        /// This will be called if a TaskAdded event is raised in one of TaskStore's Projects.
        /// </summary>
        /// <param name="e"><see cref="TaskEventArgs"/> for the event</param>
        /// <param name="sender">The sender of the event (Should be the Project that contains the Task)</param>
        private static void OnTaskAdded(Task newTask)
        {
            if (TaskAdded != null)
            {
                TaskAdded(null, new TaskEventArgs(newTask));
            }
        }

        /// <summary>
        /// Raises the TaskDeleted event.
        /// This will be called if a TaskDeleted event is raised in one of TaskStore's Projects.
        /// </summary>
        /// <param name="e">The <see cref="TaskEventArgs"/> for the event.</param>
        /// <param name="sender">The sender of the event (Should be the Project that contains the Task).</param>
        private static void OnTaskDeleted(Task deletedTask)
        {
            if (TaskDeleted != null)
            {
                TaskDeleted(null, new TaskEventArgs(deletedTask));
            }
        }


        private static void OnTaskPropertiesUpdated(object sender, PropertyChangedEventArgs e)
        {
            if (TaskPropertyChanged != null)
            { TaskPropertyChanged(sender, e); }
        }


        private static void SyncService_ActionStarted(object sender, EventArgs e)
        {
            enableSaving = false;
        }


        private static void SyncService_ActionCompleted(object sender, EventArgs e)
        {
            enableSaving = true;
            save();
        }


        private static void Project_IdChanged(object sender, IdChangedEventArgs e)
        {
            rebuildProjectDictionary();
            updateIdinChanges(typeof(Project), e.OldId, (sender as Project).Id);

            List<Task> projectsTasks = (from task in tasks.Values
                                        where task.ProjectId == e.OldId
                                        select task).ToList<Task>();

            foreach (Task t in projectsTasks)
            {
                t.ProjectId = (sender as Project).Id;
            }

        }

        private static void Task_IdChanged(object sender, IdChangedEventArgs e)
        {
            rebuildTaskDictionary();
            updateIdinChanges(typeof(Task), e.OldId, (sender as Task).Id);
        }


        private static void Project_ProjectPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            Change c = new Change()
            {
                ChangedItemType = typeof(Project),
                ChangeTime = DateTime.Now,
                Kind = ChangeKind.Modified,
                ItemId = (sender as Project).Id,                
                PropertyName = e.PropertyName
            };
            AddChange(c);

            save();
        }

        private static void Task_TaskPropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            Change c = new Change()
            {
                ChangedItemType = typeof(Task),
                ChangeTime = DateTime.Now,
                Kind = ChangeKind.Modified,
                ItemId = (sender as Task).Id,                
                PropertyName = e.PropertyName
            };
            AddChange(c);

            OnTaskPropertiesUpdated(sender, e);

            save();
        }


        private static void updateIdinChanges(Type changedItemType, string oldId, string newId)        
        {
            List<Change> matchingChanges = (from change in changes
                                            where change.ChangedItemType == changedItemType && change.ItemId == oldId
                                            select change).ToList<Change>();

            foreach (Change c in matchingChanges)
            {
                c.ItemId = newId;
            }

            save();
        }



        private static void Change_PropertyChanged(object sender, PropertyChangedEventArgs e)
        {
            save();
        }


        private static void rebuildProjectDictionary()
        {
            List<Project> projectList = projects.Values.ToList<Project>();
            projects.Clear();
            foreach (Project p in projectList)
            {
                projects.Add(p.Id, p);
            }
        }

        private static void rebuildTaskDictionary()
        {
            List<Task> taskList = tasks.Values.ToList<Task>();
            tasks.Clear();
            foreach (Task t in taskList)
            {
                tasks.Add(t.Id, t);
            }
        }



        private static void setLogger()
        {
            if (ApplicationSettings.Settings.Logging_Enable)
                LoggerService.FactoryResolver = new SimpleFactoryResolver(new SLF.NLogFacade.NLogLoggerFactory());
            else
                LoggerService.SetLogger(null); 
        }


        private static void ApplicationEvents_ApplicationClosing(object sender, EventArgs e)
        {
            save(true);
        }


        /// <summary>
        /// Saves changes to file using an instance of Fileaccess.
        /// For performance reasons data will only be written to disk if the time between lastWrite and DateTime.Now is at least as great as specified in <see cref="ApplicationSettigns.Settings.TaskStore_WriteInterval"/>.
        /// </summary>
        /// <param name="overrideWriteInterval">Forces the data to be written to disk even if time passed since the last time the file was updated is smaller than the specified WriteInterval. Default value is false.</param>
        private static void save(bool overrideWriteInterval = true)
        {

            if (enableSaving &&(overrideWriteInterval || (DateTime.Now - lastWrite) > ApplicationSettings.Settings.TaskStore_WriteInterval))
            {
                fileaccess.SaveData(GetProjects(), GetTasks(), GetChanges());              
            }
        }
   


        #endregion
    }
}